<?php
/**
 * User: toozy <toozy@foxmail.com>
 * Date: 13-5-31
 * Time: 下午1:54
 */
ignore_user_abort(1);
set_time_limit(0);
ini_set('display_errors','0');
/**
 * Class Bingo
 */
class Bingo {
    /**
     * log实例
     * @var Log
     */
    public static $log;
    /**
     * 异常报告实例
     * @var Report
     */
    public static $report;
    /**
     * 系统名称
     * @var
     */
    public static $name;

    /**
     * 是否有访问权限
     * @var bool
     */
    public static $_hasPermission = true;

    /**
     * 存放Bingo目录 ，app目录
     * @var array
     */
    protected static $_path = array();
    /**
     * 钩子
     * @var array
     * @see Bingo::bindHook
     * @see Bingo::hook
     */
    protected static $_hooks = array(
        'error'=>array(),
        'shutdown'=>array(),
        'route'=>array(),
        'config'=>array(),
        'load'=>array(),
        'permission'=>array(),
        'withoutPermission'=>array(),
    );

    /**
     * 系统初始化
     * @param string $appDir app目录
     */
    public static function run($appDir){
        #gpc的统统的消灭
        self::$_gpcCookie = $_COOKIE;
        self::$_gpcGet = $_GET;
        self::$_gpcPost = $_POST;
        unset($_COOKIE,$_GET,$_POST,$_REQUEST);
        #系统以及应用各自的根目录
        self::$_path['bingo'] = realpath(dirname(__DIR__));
        self::$_path['app'] = $appDir;
        #主配置
        $config = self::config('main');
        #应用名称
        self::$name = $config['app_name'];
        #全局变量
        self::$_g = $config['global'];
        #设置自动加载,app的文件可以覆盖掉bingo的文件
        set_include_path(
            self::$_path['app'].'/core'.PATH_SEPARATOR.
            self::$_path['bingo'].'/core'.PATH_SEPARATOR.
            self::$_path['app'].'/model'.PATH_SEPARATOR.
            self::$_path['app'].'/extensions'.PATH_SEPARATOR.
            self::$_path['bingo'].'/extensions'.PATH_SEPARATOR.
            self::$_path['app'].'/hook'.PATH_SEPARATOR.
            self::$_path['bingo'].'/hook'.PATH_SEPARATOR.
            self::$_path['app'].'/controllers'.PATH_SEPARATOR.
            get_include_path()
        );
        spl_autoload_register(array('Bingo','_autoLoad'));
        #注册错误,异常，脚本退出处理方法,致命的异常和错误在脚本结束时捕获
        set_error_handler(array('Bingo','errorHandler'));
        register_shutdown_function(array('Bingo','shutdownHandler'));
        #初始日志记录，并注册到错误以及全局脚本退出监听列表
        isset($config['log']['class']) or $config['log']['class'] = 'Log';
        self::$log = self::load(
            $config['log']['class'],array($config['log']['params'])
        );
        self::bindHook('error',array(self::$log,'error'));
        #初始异常报告，并注册到错误以及全局脚本退出监听列表
        isset($config['report']['class']) or $config['report']['class'] = 'Report';
        self::$report = self::load(
            $config['report']['class'],array($config['report']['params'])
        );
        self::bindHook('error',array(self::$report,'error'));
        #加载配置文件里的钩子
        self::bindHookByConfig($config['hooks']);
        #路由
        self::_route();
        #组件信息
        self::$component = new stdClass;
        self::$_componentConf = $config['components'];
        #检测权限
        self::_checkPermission();
        if (!Bingo::$_hasPermission) {
            return ;
        }
        #执行
        self::_execute();
    }

    protected static function _checkPermission(){
        Bingo::hook('permission');
        if (!Bingo::$_hasPermission) {
            Bingo::hook('withoutPermission');
        }
    }

    public static function _execute(){
        #添加MVC后缀
        self::_addMVCSuffix();

        if (self::$_routeInfo['m']) {
            self::m()->execute();
        } else if (self::$_routeInfo['c']) {
            self::c()->execute();
        } else {
            self::a()->init()->execute();
        }
    }

    public static function to($a,$c = null,$m = null){
        self::$_routeInfo = array(
            'm'=>$m,
            'c'=>$c,
            'a'=>$a
        );
        self::_execute();
    }

    public static  function _autoLoad($name){
        $file = $name.'.php';
        return @include $file;
    }

    /**
     * 获取类似 app.model.Index 等句号路径表达式对应的真实地址
     * @param $path
     * @return string
     */
    public static function path($path){
        if(!is_array($path)) {
            $pathInfo = explode('.',$path);
        } else {
            $pathInfo = $path;
        }
        if (count($pathInfo) == 1) {
            return $pathInfo[0];
        }else if ($pathInfo[0] == 'app' || $pathInfo[0] == 'bingo'){
            $pathInfo[0] = self::$_path[$pathInfo[0]];
        } else {
            array_unshift($pathInfo,self::$_path['app']);
        }
        return implode(DIRECTORY_SEPARATOR,$pathInfo);
    }

    /**
     * 监听错误，并触发error钩子
     * @param $errNo
     * @param $errStr
     * @param $errFile
     * @param $errLine
     * @param $errBacktrace
     */
    public static function errorHandler(
        $errNo, $errStr, $errFile, $errLine,$errBacktrace
    ){
        $e['type'] = $errNo;
        $e['message'] = $errStr;
        $e['file'] = $errFile;
        $e['line'] = $errLine;
        $e['backtrace'] = $errBacktrace;
        self::hook('error',array($e));
    }

    /**
     * 监听脚本退出，触发shutdown钩子，
     * 存在错误时触发error钩子，
     * 并在未刷出视图时显示错误页面
     */
    public static function shutdownHandler(){
        self::hook('shutdown',array());
        $e = error_get_last();
        if (!is_null($e)) {
            self::hook('error',array($e));
            if (self::$_routeInfo){
                try {
                    $a = self::a();
                    try{
                        $a->exception($e);
                    } catch (Exception $_e) {
                        if ($a->state != 'finished') {
                            throw $_e;
                        }
                    }
                } catch (Exception $_e) {
                    self::show('bingo.view.error',$e);
                }
            } else {
                self::show('bingo.view.error',$e);
            }
        }

    }

    /**
     * 渲染视图
     * @param string $_file 视图文件，句号格式如bingo.view.error，test表示app.view.test
     * @param array $_params 传给视图的数据
     * @param bool $_return 是否返回视图字符串，或是直接输出
     * @return string|null
     */
    public static function show($_file,$_params,$_return = false){
        if (
            isset($_params['_file']) or
            isset($_params['_return']) or
            isset($_params['_params'])
        ) {
            exit('传给模版的数据数组不能含有_file,_params或_return键名');
        }
        foreach ($_params as $key=>$value) {
            ${$key} = $value;
        }
        if ($_return) {
            ob_get_clean();
            ob_start();
        }
        $viewFile = self::path($_file).'.php';
        require $viewFile;
        if ($_return) {
            $content = ob_get_contents();
            ob_end_clean();
            return $content;
        }
        return null;
    }

    const HOOK_BREAK = 1;
    const HOOK_OK = 2;
    const HOOK_FAIL = 4;
    /**
     * 绑定一个回调到一个钩子上，如钩子原来不存在则会创建
     * @param string $hookName
     * @param callback $callBack
     */
    public static function bindHook($hookName,$callBack){
        self::$_hooks[$hookName][] = $callBack;
    }

    /**
     * 将配置文件的hook绑定
     * @param $confHooks
     * @notice 要求绑定的回调的类必须是 app.hook下或bingo.hook下的类，
     *         原类名必须以Hook结尾，但是写在配置文件时不用写结尾的Hook,
     *         例如：['test'=>[['test','method']]]里的绑定方法会被处理为：
     *         TestHook::method
     * @notice 类名不可以是句点格式，默认会先从app的hook目录查找，
     *         再去bingo的hook目录查找
     */
    public static function bindHookByConfig($confHooks){
        foreach ($confHooks as $hookName=>$callBacks){
            foreach ($callBacks as $callBack) {
                if (is_array($callBack) and $callBack){

                    $callBack[0] = ucfirst($callBack[0]).'Hook';
                } else if (is_null($callBack)) {
                    self::$_hooks[$hookName] = array();
                }
                self::bindHook($hookName,$callBack);
            }
        }
    }

    /**
     * 触发一个钩子
     * @param $name
     * @param array $params
     * @throws HookNotExistsError
     */
    public static function hook($name,$params=array()){
        if (!isset(self::$_hooks[$name])) {
            throw new HookNotExistsError("hook:{$name}不存在");
        }
        foreach (self::$_hooks[$name] as $listener) {
            try {
                $rs = @call_user_func_array(
                    $listener,
                    $params
                );
                if ($rs == Bingo::HOOK_BREAK) {
                    break;
                }
            } catch (Exception $e) {
                $paramsStr = json_encode($params);
                if (is_array($listener)) {
                    $listenerStr = json_encode($listener);
                } else {
                    $listenerStr = $listener;
                }
                syslog(
                    LOG_ALERT,
                    "hook({$name},{$paramsStr})::{$listenerStr}::".$e->getMessage()
                );
            }
        }
    }

    /**
     * 获取module，如果有的话
     * @return Module|null
     */
    public static function m(){
        static $_ = null;
        if (is_null($_)) {
            $mName = ucfirst(self::$_routeInfo['m']);
            $mClass = 'app.controllers.'.self::$_routeInfo['m'].'.'.$mName;
            $_ = self::load($mClass);
        }
        return $_;
    }

    /**
     * 获取Controller，如果有的话
     * @return Controller|null
     */
    public static function c(){
        static $_ = null;
        if (is_null($_)) {
            $cName = ucfirst(self::$_routeInfo['c']);
            if (self::$_routeInfo['m']) {
                $mName = self::$_routeInfo['m'];
                $cClass = 'app.controllers.'.$mName.'.'.self::$_routeInfo['c'].
                    '.'.$cName;
            } else {
                $cClass = 'app.controllers.'.self::$_routeInfo['c'].'.'.$cName;
            }
            $_ = self::load($cClass,func_get_args());
        }
        return $_;
    }

    /**
     * 获取Action，如果初始话正常并已经完成了的话
     * @return Action|null
     */
    public static function a(){
        static $_ = null;
        if (is_null($_)) {
            $aName = ucfirst(self::$_routeInfo['a']);
            if (self::$_routeInfo['m']) {
                $mName = self::$_routeInfo['m'];
                $cName = self::$_routeInfo['c'];
                $aClass = 'app.controllers.'.$mName.'.'.$cName.'.'.$aName;
            } else if (self::$_routeInfo['c']) {
                $cName = self::$_routeInfo['c'];
                $aClass = 'app.controllers.'.$cName.'.'.$aName;
            } else {
                $aClass = 'app.controllers.'.$aName;
            }
            $_ = self::load($aClass,func_get_args());
        }
        return $_;
    }

    /**
     * 记录m,c,a对应的名称
     * @var array
     * @see Bingo::route
     */
    public static $_routeInfo = array();
    public static function getRouteInfo(){
        return self::$_routeInfo;
    }
    protected static function _route(){
        self::validate(array(
            array('name'=>'~m','type'=>'get','request'=>false),
            array('name'=>'~c','type'=>'get','request'=>false),
            array('name'=>'~a','type'=>'get','request'=>false)
        ));
        if (self::param('~a','get')) {
            self::$_routeInfo['m'] = self::param(
                '~m','get',self::PARAM_MODE_IMPORT
            );
            self::$_routeInfo['c'] = self::param(
                '~c','get',self::PARAM_MODE_IMPORT
            );
            self::$_routeInfo['a'] = self::param(
                '~a','get',self::PARAM_MODE_IMPORT
            );
        } else {
            $config = self::config('main');
            self::$_routeInfo = $config['default_route'];
        }
        self::hook('route');
    }

    /**
     * @throws GPCError
     */
    protected static function _addMVCSuffix(){
        if (!isset(self::$_routeInfo['a'])) {
            throw new GPCError("类型:get,名称:~a的gpc不存在。");
        } else {
            self::$_routeInfo['a'] .= 'Action';
        }
        if (isset(self::$_routeInfo['m']) and self::$_routeInfo['m']) {
            self::$_routeInfo['m'] .= 'Module';
        } else {
            self::$_routeInfo['m'] = null;
        }
        if (isset(self::$_routeInfo['c']) and self::$_routeInfo['c']) {
            self::$_routeInfo['c'] .= 'Controller';
        } else {
            self::$_routeInfo['c'] = null;
        }
    }

    /**
     * 存储get
     * @var array
     */
    protected static $_gpcGet = array();
    /**
     * 存储post
     * @var array
     */
    protected static $_gpcPost = array();
    /**
     * 存储cookie
     * @var array
     */
    protected static $_gpcCookie = array();
    /**
     * 存储gpc经过validate的键名
     * @var array
     * @see Bingo::validate
     * @see Bingo::param
     */
    protected static $_whiteList = array(
        'cookie'=>array(),
        'post'=>array(),
        'get'=>array()
    );
    /**
     * 只有通过验证的变量才能被param获取到
     * @param array $rule
     *      array(
     *          array(
     *              'name'=>'name',
     *              'type'=>'type',
     *              'validator'=>'校验函数|正则',
     *              'params'=>'参数',
     *              'request'=>'是否必须'
     *          ),
     *          ...
     *      );
     * @throws GPCError
     */
    public static function validate($rule){
        $params = array();
        foreach ($rule as $paramRule) {
            if (!isset($params[$paramRule['type']])) {
                $params[$paramRule['type']] = self::${'_gpc'.ucfirst($paramRule['type'])};
            }

            if (isset($params[$paramRule['type']][$paramRule['name']])) {
                $value = $params[$paramRule['type']][$paramRule['name']];
                if (!isset($paramRule['validator']) or !$paramRule['validator']) {
                    self::${'_gpc'.ucfirst($paramRule['type'])}[$paramRule['name']] = $value;
                } else if (is_callable($paramRule['validator'])) {
                    if (!isset($paramRule['params'])) {
                        $vRs = call_user_func($paramRule['validator'],$value);
                    } else {
                        $vRs = call_user_func_array(
                            $paramRule['validator'],
                            array($value,'params'=>$paramRule['params'])
                        );
                    }
                    if (!$vRs){
                        throw new GPCError(
                            "类型:{$paramRule['type']},名称:{$paramRule['name']}不能通过校验函数"
                        );
                    }
                } else {
                    if (!preg_match($paramRule['validator'],$value)) {
                        throw new GPCError(
                            "类型:{$paramRule['type']},名称:{$paramRule['name']}不能通过校验正则"
                        );
                    }
                }
            } else if($paramRule['request']) {
                throw new GPCError(
                    "类型:{$paramRule['type']},名称:{$paramRule['name']}的gpc不存在。"
                );
            } else {
                self::${'_gpc'.ucfirst($paramRule['type'])}[$paramRule['name']] = null;
            }
            self::$_whiteList[$paramRule['type']][] = $paramRule['name'];
        }
    }

    const PARAM_MODE_NORMAL = 0; //不做处理
    const PARAM_MODE_PATH = 1; //去掉目录分割符,其实可以不用处理的
    const PARAM_MODE_HTML = 2; //转义html标签和引号
    const PARAM_MODE_SQL = 4; //转义引号
    const PARAM_MODE_INT = 8; //intval
    const PARAM_MODE_IMPORT = 16; //用于作为文件包含的
    const PARAM_MODE_TRIM = 1048576; //处理值之前，trim一下
    const PARAM_MODE_FUNC = 2097152; //用后面的函数处理一下
    /**
     * 获取外部参数
     * @param string $name key
     * @param string $type get|post|cookie
     * @param int $mode 模式
     * @param string|callback $callback 回调函数
     * @param array $params
     *      回调函数的参数 call_user_func_array($callback,array($v,'params'=>$params));
     * @return int|mixed|string
     * @throws GPCError
     * @notice 外部参数必须是经过 Bingo::validate 处理过的
     */
    public static function param($name,$type,$mode = 0,$callback = '',$params = array()){
        if (!isset(self::$_whiteList[$type])) {
            throw new GPCError(
                "类型:{$type},名称:{$name}的gpc未通过校验。"
            );
        }
        $v = self::${'_gpc'.ucfirst($type)}[$name];
        if ($mode&self::PARAM_MODE_TRIM) {
            $v = trim($v);
        }
        if ($mode&self::PARAM_MODE_FUNC) {
            if ($params) {
                $v = call_user_func_array($callback,array($v,'params'=>$params));
            } else {
                $v = call_user_func($callback,$v);
            }
        }
        if ($mode&self::PARAM_MODE_PATH) {
            $v = str_replace(array('/','\\'),'',$v);
        } else if ($mode&self::PARAM_MODE_IMPORT) {
            $v = preg_replace('/\{2,}/','.',$v);
        } else if ($mode&self::PARAM_MODE_SQL) {
            $v = addslashes($v);
        } else if ($mode&self::PARAM_MODE_HTML) {
            $v = htmlentities($v,ENT_QUOTES);
        }else if ($mode&self::PARAM_MODE_INT) {
            $v = intval($v);
        }
        return $v;
    }

    /**
     * 组件类，推荐使用 Bingo::getComponent
     * @var
     */
    public static $component;
    /**
     * @var array 组件的配置
     */
    protected static $_componentConf = array();

    /**
     * 获取一个组件，组件未获取前是未初始化的
     * @param $name
     * @return mixed
     * @throws AppNotHasComponentError
     */
    public static function getComponent($name){
        if (property_exists(self::$component,$name)) {
            return self::$component->$name;
        } else if (isset(self::$_componentConf[$name])) {
            if (isset(self::$_componentConf[$name]['params'])) {
                $param = array(self::$_componentConf[$name]['params']);
            } else {
                $param = array();
            }

            if (isset(self::$_componentConf[$name]['class'])) {
                $class = self::$_componentConf[$name]['class'];
                self::$component->$name = self::load($class,$param,false);
            } else {
                try {
                    $class = 'app.components.'.ucfirst($name);
                    $obj = self::load($class,$param,false);
                } catch (LoadClassNotExistsError $e) {
                    try {
                        $class = 'bingo.components.'.ucfirst($name);
                        $obj = self::load($class,$param,false);
                    } catch (LoadClassNotExistsError $e) {
                        throw new AppNotHasComponentError("组件{$name}不存在");
                    }
                }
                self::$component->$name = $obj;
            }
            unset(self::$_componentConf[$name]);
            return self::$component->$name;
        } else {
            try {
                $class = 'app.components.'.ucfirst($name);
                $obj = self::load($class,null,false);
            } catch (LoadClassNotExistsError $e) {
                try {
                    $class = 'bingo.components.'.ucfirst($name);
                    $obj = self::load($class,null,false);
                } catch (LoadClassNotExistsError $e) {
                    throw new AppNotHasComponentError("组件{$name}不存在");
                }
            }
            self::$component->$name = $obj;
            return self::$component->$name;
        }
    }

    /**
     * load已经实例化过的类集合
     * @var array
     */
    protected static $_instance = array();
    /**
     * @var string $loading 目前加载中的类名、参数，可以在hook中改变
     */
    public static $_loading = array(
        'class'=>'',
        'params'=>'',
        'className'=>'',
    );

    /**
     * 加载并实例化类
     * @param string $class 类名称，支持句号模式
     * @param null $params 类参数
     * @param bool $single 是否单例
     * @return object
     */
    public static function load($class,$params = null,$single = true){
        if ($single && isset(self::$_instance[$class])) {
            return self::$_instance[$class];
        }
        self::$_loading['class'] = $class;
        self::$_loading['params'] = $params;
        $_tempClassName = strrchr($class,'.');
        self::$_loading['className'] = $_tempClassName?
            substr($_tempClassName,1):$class;
        self::hook('load',array());
        $obj = self::_load();
        if ($single) {
            self::$_instance[$class] = $obj;
        }
        self::$_loading = array(
            'class'=>'',
            'params'=>array()
        );
        return $obj;
    }
    protected static function _load(){
        $class = self::$_loading['className'];
        $file = self::path(self::$_loading['class']).'.php';
        if (file_exists($file)) {
            require $file;
        }
        if (!class_exists($class)) {
            throw new LoadClassNotExistsError("指定的类 {$class}不存在。");
        }

        if (self::$_loading['params']) {
            $classReflection = new ReflectionClass($class);
            $obj = $classReflection->newInstanceArgs(self::$_loading['params']);
        } else {
            $obj = new $class();
        }

        return $obj;
    }

    /**
     * @var array 存储配置
     */
    protected static $_conf = array();
    /**
     * @var string 加载中的config的路径，可在hook中改变
     */
    public static $_loadingConf = '';

    /**
     * 加载并返回config
     * @param string $config 配置路径，句号路径
     * @return mixed
     */
    public static function config($config){
        if (isset(self::$_conf[$config])) {
            return self::$_conf[$config];
        }
        self::$_loadingConf = $config;
        self::hook('config',array());
        if (strpos(self::$_loadingConf,'.') === false) {
            self::$_loadingConf = 'app.config.'.self::$_loadingConf;
        }
        $file = self::path(self::$_loadingConf).'.php';
        self::$_conf[$config] = @include $file;
        self::$_loadingConf = '';
        return self::$_conf[$config];
    }

    #全局变量存放
    protected static $_g = array();

    /**
     * 获取全局参数，只有一个参数时为获取，两个参数时为设置
     * @return mixed
     * @throws GlobalKeyNotExistsError
     * @notice 只支持按传入键值获取对应的键值。例如：
     *         Bingo::g('test.a','a');
     *         Bingo::g('test.b','b');
     *         Bingo::g('test');//throw GlobalKeyNotExistsError
     *         //test.a test.b test 3者是没关系的
     */
    public static function g(){
        if (func_num_args() == 2) {
            $key = func_get_arg(0);
            $value = func_get_arg(1);
            self::$_g[$key] = $value;
        } else {
            $key = func_get_arg(0);
            if (isset(self::$_g[$key])) {
                return self::$_g[$key];
            } else {
                throw new GlobalKeyNotExistsError("{$key}不存在");
            }
        }
        return null;
    }

    /**
     *
     * @param $key
     * @return bool
     */
    public static function hasGlobal($key){
        return isset(self::$_g[$key]);
    }
}

/**
 * Class APPNOTHASCOMPONENTERROR
 * 组件不存在错误
 */
class AppNotHasComponentError extends Exception{}

/**
 * Class AppComponentInitError
 * 组件初始化失败
 */
class AppComponentInitError extends Exception{}

/**
 * Class HookNotExistsError
 * 钩子不存在
 */
class HookNotExistsError extends Exception{}

/**
 * Class LoadInitError
 * load初始化对象的时候错误
 */
class LoadInitError extends Exception{}

/**
 * Class LoadClassNotExistsError
 * 加载类文件不存在
 */
class LoadClassNotExistsError extends Exception{}

/**
 * Class GlobalKeyNotExistsError
 * 试图获取的Bingo::$_g[$key]不存在
 */
class GlobalKeyNotExistsError extends Exception{}

/**
 * Class GPCError
 */
class GPCError extends Exception{}